<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/metrics/all_metrics.html">
<link rel="import" href="/tracing/metrics/metric_registry.html">
<link rel="import" href="/tracing/mre/failure.html">
<link rel="import" href="/tracing/mre/function_handle.html">
<link rel="import" href="/tracing/value/diagnostics/reserved_names.html">
<link rel="import" href="/tracing/value/histogram_set.html">

<script>
'use strict';

tr.exportTo('tr.metrics', function() {
  /**
   * @param {!tr.model.Model} model
   * @param {!Object} options
   * @param {!Array.<string>} options.metrics
   * @param {!Function} addFailureCb
   * @return {!tr.v.HistogramSet}
   */
  function runMetrics(model, options, addFailureCb) {
    if (options === undefined) {
      throw new Error('Options are required.');
    }

    const metricNames = options.metrics;
    if (!metricNames) {
      throw new Error('Metric names should be specified.');
    }

    const allMetricsStart = new Date();
    const durationBreakdown = new tr.v.d.Breakdown();

    const histograms = new tr.v.HistogramSet();

    histograms.createHistogram('trace_import_duration',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
        model.stats.traceImportDurationMs, {
          binBoundaries: tr.v.HistogramBinBoundaries.createExponential(
              1e-3, 1e5, 30),
          description:
            'Duration that trace viewer required to import the trace',
          summaryOptions: tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS,
        });

    for (const metricName of metricNames) {
      const metricStart = new Date();
      const metric = tr.metrics.MetricRegistry.findTypeInfoWithName(metricName);
      if (metric === undefined) {
        throw new Error(`"${metricName}" is not a registered metric.`);
      }
      try {
        metric.constructor(histograms, model, options);
      } catch (e) {
        const err = tr.b.normalizeException(e);
        addFailureCb(new tr.mre.Failure(
            undefined, 'metricMapFunction', model.canonicalUrl, err.typeName,
            err.message, err.stack));
      }
      const metricMs = new Date() - metricStart;
      histograms.createHistogram(
          metricName + '_duration',
          tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
          [metricMs]);
      durationBreakdown.set(metricName, metricMs);
    }

    validateDiagnosticNames(histograms);

    const allMetricsMs = new Date() - allMetricsStart +
      model.stats.traceImportDurationMs;
    durationBreakdown.set('traceImport', model.stats.traceImportDurationMs);
    durationBreakdown.set('other', allMetricsMs - tr.b.math.Statistics.sum(
        durationBreakdown, ([metricName, metricMs]) => metricMs));
    const breakdownNames = tr.v.d.RelatedNameMap.fromEntries(new Map(
        metricNames.map(metricName => [metricName, metricName + '_duration'])));
    breakdownNames.set('traceImport', 'trace_import_duration');
    histograms.createHistogram(
        'metrics_duration',
        tr.b.Unit.byName.timeDurationInMs_smallerIsBetter,
        [
          {
            value: allMetricsMs,
            diagnostics: {breakdown: durationBreakdown},
          },
        ], {
          diagnostics: {breakdown: breakdownNames},
        });

    return histograms;
  }

  /**
   * Ensure that metrics don't use reserved diagnostic names.
   *
   * @param {!tr.v.HistogramSet} histograms
   */
  function validateDiagnosticNames(histograms) {
    for (const hist of histograms) {
      for (const name of hist.diagnostics.keys()) {
        if (name === tr.v.d.RESERVED_NAMES.ALERT_GROUPING) {
          // Metrics can set alert grouping when they create histogram. It's
          // still a reserved diagnostic because that helps us enforce the right
          // diagnostic shape.
          continue;
        }
        if (tr.v.d.RESERVED_NAMES_SET.has(name)) {
          throw new Error(
              `Illegal diagnostic name "${name}" on Histogram "${hist.name}"`);
        }
      }
    }
  }

  /**
   * @param {!tr.v.HistogramSet} histograms
   * @param {!tr.model.Model} model
   */
  function addTelemetryInfo(histograms, model) {
    for (const metadata of model.metadata) {
      if (!metadata.value || !metadata.value.telemetry) continue;

      for (const [name, value] of Object.entries(metadata.value.telemetry)) {
        const type = tr.v.d.RESERVED_NAMES_TO_TYPES.get(name);
        if (type === undefined) {
          throw new Error(`Unexpected telemetry.${name}`);
        }
        histograms.addSharedDiagnosticToAllHistograms(name, new type(value));
      }
    }
  }

  /**
   * @param {!tr.mre.MreResult} result
   * @param {!tr.model.Model} model
   * @param {!Object} options
   * @param {!Array.<string>} options.metrics
   */
  function metricMapFunction(result, model, options) {
    const histograms = runMetrics(
        model, options, result.addFailure.bind(result));
    addTelemetryInfo(histograms, model);

    if (model.canonicalUrl !== undefined) {
      const info = tr.v.d.RESERVED_INFOS.TRACE_URLS;
      histograms.addSharedDiagnosticToAllHistograms(
          info.name, new info.type([model.canonicalUrl]));
    }

    result.addPair('histograms', histograms.asDicts());

    const scalarDicts = [];
    for (const value of histograms) {
      for (const [statName, scalar] of value.statisticsScalars) {
        scalarDicts.push({
          name: value.name + '_' + statName,
          numeric: scalar.asDict(),
          description: value.description,
        });
      }
    }
    result.addPair('scalars', scalarDicts);
  }

  tr.mre.FunctionRegistry.register(metricMapFunction);

  return {
    metricMapFunction,
    runMetrics,
  };
});
</script>
